module PeerCafe
  BootstrapRequest = Struct.new(:src, :topic_nids)
  BootstrapResponse = Struct.new(:bootstrap_nids)

  class Client < GoSim::Net::Node
    @@log = Logger.new(STDOUT)
    @@log.level = Logger::INFO
    attr_accessor :master_nid, :bootstrap_nids, :topics

    def initialize(profiles, master_nid)
      super()
      @topics = []
      @master_nid = master_nid

      profiles.each do |profile|
        @topics << Topic.new(self, profile)
      end

      @topics.each {|t| t.find_family }

      bootstrap
    end

    def bootstrap
      topic_nids = @topics.collect {|a| a.nid }
      send_packet(@master_nid, BootstrapRequest.new(@nid, topic_nids))
    end

    def handle_failed_packet(pkt)
      @@log.debug "Failed Packet from #{@nid} to #{pkt.dest}: #{pkt.data.class}"
    end

    def handle_bootstrap_response(pkt)
      @bootstrap_nids = pkt.bootstrap_nids
      @@log.debug "#{nid} - got bootstrap peers:\n" + @bootstrap_nids.join(", ")

      # Now that we have nids the topics can start searching for their homes
      @topics.each {|a| a.find_home(@bootstrap_nids) }
    end

    def found_home(topic)
      @@log.debug("Client #{@nid} had topic #{topic.nid} find home!!!!!!!!!!!!!!!!!!!!!!!!")
    end
  end

  ProfileRequest = Struct.new(:src, :topic_nid, :profile)
  ProfileResponse = Struct.new(:profile)

  class Topic < GoSim::Net::Node
    @@log = Logger.new(STDOUT)
    @@log.level = Logger::INFO

    @@topic_data = GoSim::DataSet.new(:topic, Base.pc_dir("output"))

    SEARCH_UPDATE_TIME = 30
    NEIGHBORHOOD_UPDATE_TIME = 90
    NEIGHBORHOOD_SIZE = 3

    attr_accessor :profile

    def initialize(client, profile)
      super()
      @client = client
      @profile = profile
      @peers = {}
      @neighbors = []

      @@topic_data.log(@sid, :new, profile)
      @@log.debug "#{@nid} - Topic up: " + profile.join(' ')
    end

    def find_family
      @family = @client.topics - [self]
      @@topic_data.log(@sid, :family, @family)
    end

    # Start the process of looking for our cluster.
    def find_home(peer_ids)
      peer_ids.each do |peer_id|
        @peers[peer_id] = Peer.new(self, peer_id)
      end

      # Update the neighborhood periodically
      set_timeout(SEARCH_UPDATE_TIME, true) { update_neighborhood }
    end

    def meet_neighbors
      @neighbors.each {|n| n.request_neighbors }
    end

    def update_neighborhood
      puts "Updating neighborhood #{@sim.time}"
      ordered_peers = @peers.values.sort_by {|p| p.distance }

      # Now cut down to neighborhood size 
      new_neighbors = ordered_peers[0, NEIGHBORHOOD_SIZE]

      (ordered_peers - new_neighbors).each {|p| p.kill }
      (@neighbors - new_neighbors).each {|n| n.peer }

      # Keep the neighbors in the peer hash for comparison with the next round.
      @peers.clear
      new_neighbors.each {|n| @peers[n.peer_id] = n}

      if @neighbors == new_neighbors
        # Update the neighborhood at a slower rate
        set_timeout(NEIGHBORHOOD_UPDATE_TIME, true) { update_neighborhood }

        return false
      else
        @neighbors = new_neighbors
        @neighbors.each {|n| n.neighbor }

        meet_neighbors
        
        true
      end
    end

    def got_neighbors(neighbors)
      neighbors.each {|n| add_peer(n) }
    end

    # Just respond with the profile.
    def handle_profile_request(pkt)
      add_peer(pkt.topic_nid, pkt.profile)
      send_packet(pkt.src, ProfileResponse.new(@profile.clone))
    end

    def handle_neighbor_request(pkt)
      send_packet(pkt.src, 
                  NeighborResponse.new( @neighbors.collect {|n| n.peer_id } +
                                        @client.topics.collect {|t| t.nid } - [@nid]))
    end

    def add_peer(peer_id, profile=nil)
      unless @peers[peer_id]
        @peers[peer_id] = Peer.new(self, peer_id, profile)
      end
    end
  end

  NeighborRequest = Struct.new(:src)
  NeighborResponse = Struct.new(:neighbors)

  class Peer < GoSim::Net::Node
    @@log = Logger.new(STDOUT)
    @@log.level = Logger::INFO

    @@peer_data = GoSim::DataSet.new(:peer, Base.pc_dir("output"))

    attr_accessor :distance, :peer_id, :profile

    def initialize(topic, peer_id, profile=nil)
      super()
      @topic = topic
      @peer_id = peer_id
      @distance = 9999999 
      @profile = profile

      # We always start with a profile request, unless we have it already
      if @profile
        @distance = compute_distance(@profile)
      else
        send_packet(@peer_id, ProfileRequest.new(@nid, @topic.nid, @topic.profile.clone))
      end

      @@peer_data.log(@nid, :new, @topic.nid, @peer_id)
    end

    def handle_profile_response(pkt)
      @profile = pkt.profile
      @distance = compute_distance(pkt.profile)
    end

    def request_neighbors
      @@log.debug "Node #{@nid} requesting neighbors..."
      send_packet(@peer_id, NeighborRequest.new(@nid))
    end

    def handle_neighbor_response(pkt)
      @@log.debug "Node #{@nid} got neighbor response (#{pkt.neighbors.size})"
      @topic.got_neighbors(pkt.neighbors)
    end

    # Just compute euclidean distance between profiles
    def compute_distance(profile)
      return Math.sqrt((@topic.profile[0] - profile[0])**2 + 
                       (@topic.profile[1] - profile[1])**2)
    end
    
    def peer
      if @is_neighbor
        @is_neighbor = false
        @@peer_data.log(@nid, :peer) 
      end
    end

    def neighbor
      unless @is_neighbor
        @is_neighbor = true
        @@peer_data.log(@nid, :neighbor)
      end
    end

    def neighbor?
      @is_neighbor
    end

    def kill
      puts "#{@nid} peer died..."
      @@peer_data.log(@nid, :dead)
    end
  end

  class MasterPeer < GoSim::Net::Node
    PEER_MEMORY_SIZE = 10

    def initialize
      super()
      @recent_peers = []
    end

    def handle_bootstrap_request(pkt)
      send_packet(pkt.src, BootstrapResponse.new(@recent_peers.clone))
      @recent_peers.concat(pkt.topic_nids)
      if @recent_peers.size > PEER_MEMORY_SIZE
        @recent_peers = @recent_peers[@recent_peers.size - PEER_MEMORY_SIZE, PEER_MEMORY_SIZE]
      end
    end
  end
end
